home *** CD-ROM | disk | FTP | other *** search
- /* SYNC.PRG version 1.1 0/09/1993
-
- File synchronization utility by Mickey R. Burnette
- (c) 1993 Mickey R. Burnette
- (c) 1993 SofKinetics, Inc.
-
- Program placed in the public domain 6/13/1993 for Non-Commercial use
- only and provided that all copyright notices remain intact.
-
- SofKinetics, Inc. nor Mickey R. Burnette make any claims or
- warranties for this product. User shall determine suitability
- of purpose and assume all liability for product use.
-
- Compile with CLIPPER 5.2b
- clipper sync /n /l /m
- Link with RLINK
- rlink fi sync
-
- Modification History:
- 6/13/93 Version 1.0a corrected time stamp compares
- 6/22/93 Version 1.0b added aWriteOut array. Batch file created only
- when array is not empty.
- 9/09/93 Version 1.1 added Daisy-Chain target capability for mirrored
- directory structures. Added DOS wildcards to FileName field
- */
-
- // Subscripts for DIRECTORY() array
- #define F_NAME 1
- #define F_SIZE 2
- #define F_DATE 3
- #define F_TIME 4
- #define F_ATTR 5
-
- // Define file-wide static variables
- STATIC aWriteOut := {}, eTime := {}, lTime := .F., lFlag := .F.
-
- Function sync( cLine )
- #define LF chr(10)
- #define FF chr(12)
- #define CR chr(13)
- #define CRLF CR + LF
-
- local aMakDir := {}, cOutBatch := "UPDATE.BAT"
- local aDirSrc := {}, cWildSrc, cOldSrc, nSrcPtr, cSrcFile
- local aSrc := {} // for subarray
- local aDirTgt := {}, cWildTgt, cOldTgt, nTgtPtr, cTgtFile, cTgt
- local aTgt := {} // for subarray
- local lDoCopy := .F., aMakeArray, nLooper, lReportMode := .F., lVideo := .F.
- local lQuiet := .F., nCount := 0, lOverride := .F., dBaseEdit := .F.
- local cSHADOWFl, cWildSHADOW, cFileName, aFileList, aTempList, i
-
- // Convert runline to all upper case
- if ! empty( cLine )
- cLine := upper( cLine )
- if "/T" $ cLine
- aadd( eTime, "Start: " + time() )
- lTime := .T.
- endif
- endif
-
- // check for runline help
- if ! empty( cLine ) .and. "?" $ cLine
- clear screen
- ?? "File Synchronization Utility Version 1.1 (c) 1993 by SofKinetics, Inc."
- ? "Public Domain Version: Free Use License Granted. Commerical Rights Reserved."
- ?
- ? " Valid run-line parameters:"
- ?
- ? " /RP Report to printer date/time of updates"
- ? " /RV Report to screen date/time of updates"
- ? " /Q quiet mode; no video output displayed"
- ? " /O Override user intervention flag"
- ? " /M Specify location for database"
- ? " /E Edit synchronization database"
- ? " /F FileSpec passed, see below"
- ? " /T Output event timing"
- ? " /? This help screen"
- ?
- ? " * Specify multiple parameters together: /Q/O"
- ? " * When using /F, this parameter MUST be specified last"
- ? " For example: /O/Q/Fmyfile.bat"
- ? " * In /F is not specified, the default file is UPDATE.BAT"
- ?
- ? " * Note: Batch file is ONLY created when dictionary search"
- ? " identifies a file synchronization mismatch. Erase"
- ? " the batch file prior to running SYNC and test for"
- ? " its existence afterward to determine update need."
- quit
- endif
-
- // check for MemVar edit
- if ! empty( cLine ) .and. "/M" $ cLine
- mEdit()
- quit
- endif
-
- // check runline for quiet mode
- if ! empty( cLine ) .and. "/Q" $ cLine
- lQuiet := .T.
- endif
-
- // check runline for Over-Ride
- if ! empty( cLine ) .and. "/O" $ cLine
- lOverride := .T.
- endif
-
- // check runline for Edit Mode
- if ! empty( cLine ) .and. "/E" $ cLine
- dBaseEdit := .T.
- endif
-
- // check runline for Report Mode P
- if ! empty( cLine ) .and. "/RP" $ cLine
- lReportMode := .T.
- if ! isprinter()
- if alert("Cannot print to printer due;" + ;
- "to printer error! Output to;" + ;
- "video monitor instead?", ;
- {"YES","Quit"} ) == 1
- lVideo := .T.
- else
- quit
- endif
- endif
- endif
-
- // check runline for Report Mode V
- if ! empty( cLine ) .and. "/RV" $ cLine
- lReportMode := .T.
- lVideo := .T.
- endif
-
- // check for alternate batch file name
- if ! empty( cLine ) .and. "/F" $ cLine
- cOutBatch := substr(cLine, at("/F", cLine) + 2)
- if empty( cOutBatch )
- cOutBatch := "UPDATE.BAT"
- endif
- endif
-
- // get location of database from user's root
- if ! file( "C:\SYNC.MEM" )
- alert("ERROR! Dictionary pointer cannot;" + ;
- "be located on ROOT drive.", {"Press ENTER"})
- if ! mEdit()
- ERRORLEVEL(1)
- quit
- endif
- endif
-
- restore from c:\sync additive // MEMVAR->D_LOOK
-
- /* Verify that dictionary is available for use. This file could be
- on a network, so return a DOS error if not found. In this manner,
- autoexec.bat can branch around calling UPDATE.BAT if an error is
- returned: for example, when a notebook is not docked, user not
- logged into network, etc.
- */
- if ! file( D_LOOK )
- ERRORLEVEL(1)
- quit
- endif
-
- // begin main processing ...
- if ! lQuiet
- clear screen
- ? "File Synchronization Utility V1.1/Public Domain (c) 1993 by SofKinetics, Inc."
- ?
- endif
-
- /* Open dictionary file . This is a standard xBase file with
- the following file structure:
- FILENAME CHARACTER 12
- SOURCE_DIR CHARACTER 40
- TARGET_DIR CHARACTER 40
- SHADOW_DIR CHARACTER 40
- INTERACT LOGICAL
- LASTDATE DATE
- LASTTIME CHARACTER 8
- */
-
- use ( D_LOOK ) new alias dict
-
- /* If editing database, invoke editor, remove deleted records, purge and
- close DOS file buffers, and exit routine.
- */
- if dBaseEdit
- dEdit()
- close all
- quit
- endif
-
- /* Index on source directory. Since this routine is designed to run on a
- network, we would like to minimize the reading of network directories. The
- sort places the dictionary in source file order to allow us to only read
- from the network on directory changes.
- */
- index on SOURCE_DIR to c:\temp.$$$
- go top
-
- // If /R was used to indicate Report Mode, call routine and quit.
- if lReportMode
- report( lVideo )
- close all
- quit
- endif
-
- aFileList := {}
- aadd( aWriteOut, "@ECHO OFF" )
- aadd( aWriteOut, "REM File Synchronization Utility V1.1" )
- cOldSrc := "" //force initialization source path
- cOldTgt := "" //force initialization target path
-
- if lTime
- aadd( eTime, "Begin Do" + time() )
- endif
-
- Do while !eof()
- // source should be as 'drive:path\' but could be 'drive:' only
- if right(alltrim(dict->SOURCE_DIR), 1) == "\"
- cWildSrc := alltrim( dict->SOURCE_DIR ) + "*.*"
- else
- cWildSrc := alltrim( dict->SOURCE_DIR ) + "\*.*"
- endif
-
- // load source directory into source array if necessary
- if cOldSrc != cWildSrc
- aDirSrc := directory( cWildSrc )
- cOldSrc := cWildSrc
- endif
-
- // target should be as 'drive:path\' but could be 'drive:' only
- if right(alltrim(dict->TARGET_DIR), 1) == "\"
- cWildTgt := alltrim( dict->TARGET_DIR ) + "*.*"
- else
- cWildTgt := alltrim( dict->TARGET_DIR ) + "\*.*"
- endif
-
- // load target directory into target array if necessary
- if cOldTgt != cWildTgt
- aDirTgt := directory( cWildTgt )
- cOldTgt := cWildTgt
- endif
-
- // SHADOW should be as 'drive:path\' but could be 'drive:' only
- if right(alltrim(dict->SHADOW_DIR), 1) == "\"
- cWildSHADOW := alltrim( dict->SHADOW_DIR ) + "*.*"
- else
- cWildSHADOW := alltrim( dict->SHADOW_DIR ) + "\*.*"
- endif
-
- aFileList := {}
-
- /* Check for wildcards in file specification. Upon exiting this logic
- array aFileList will have one or more added entries.
- */
- if alltrim( dict->FILENAME ) == "*.*"
- aeval( aDirSrc, {| e | aadd( aFileList, e[F_NAME] )})
-
- elseif "*" $ dict->FILENAME .or. "?" $ dict->FILENAME
- if right(alltrim(dict->SOURCE_DIR), 1) == "\"
- aTempList := directory( alltrim( dict->SOURCE_DIR ) + ;
- alltrim( dict->FILENAME ))
- else
- aTempList := directory( alltrim( dict->SOURCE_DIR ) + ;
- "\" + alltrim( dict->FILENAME ))
- endif
-
- aeval( aTempList, { | aEl | aadd( aFileList, aEl[F_NAME] )})
- aTempList := {} // finished with temp array
- else
- aadd( aFileList, dict->FILENAME )
- endif
-
- /* create a loop state to handle wildcard situations. Unless an * or ?
- was utilized in the filename, the aFileList will be an array of len=1.
- Otherwise, aFileList will contain a list of entries spawned by the
- filename specification.
- */
- for i = 1 to len( aFileList )
- iif( i > 0, cFileName := aFileList[ i ], cFileName := dict->FILENAME )
-
- /* There are now two multi-dimensional arrays; ie, arrays of arrays
- Each sub-array has the structure:
- F_NAME [1]
- F_SIZE [2]
- F_DATE [3]
- F_TIME [4]
- F_ATT [5]
-
- We need only to scan the SRC array for the filename and
- extract the time and date. Then we scan the TGT array. If
- the filename is found in the target, compare the time and
- dates and copy only if SRC is newer than TGT. If filename
- is not found in the TGT, simply copy it! If a TGT filename needs
- to be updated, the check to see if a "shadow" directory was
- specified: if so, the batch file will include a copy from the TGT to
- the SHADOW. This scenario attempts to reduce network load if the
- source directory is a virtual drive.
-
- To do the scan, we'll utilize Clipper's ability to evaluate code-
- blocks for each array element. The function is aEVAL() and the
- code block, | |, will be passed two parameters: the value of the
- current element (in this case a multi-dimensional array) and the
- current offset into the array. The remaining conditional statement,
- that is the IF() will be able to access the passed parameters since
- it is part of the aEval() function. We'll just compare the filename
- from the dictionary with each filename in the array. If we find a
- match, then the source pointer is updated with the array offset.
- Then do the same thing for the target array.
- */
-
- // initialize vars
- nSrcPtr := 0
- nTgtPtr := 0
- lDoCopy := .F.
-
- // scan the source array, set nSrcPtr if filename is located
- aEval( aDirSrc, {| File, nIndex | ;
- if( alltrim(upper(cFileName)) == file[F_NAME], ;
- nSrcPtr := nIndex, NIL )})
-
- // scan the target array, set nTgtPtr if filename is located
- aEval( aDirTgt, {| File, nIndex | ;
- if( alltrim(upper(cFileName)) == file[F_NAME], ;
- nTgtPtr := nIndex, NIL )})
-
- /* Begin Sequence | End Sequence blocks are logical sections of
- code that the BREAK instruction can be used to "break-out."
- */
-
- BEGIN SEQUENCE
-
- /* if source pointer is 0, we have a serious problem! We'll tell the
- user about it, break out of the block, and go for the next item
- */
- if nSrcPtr == 0
- alert("Source file not located as defined!")
- lDoCopy := .F.
- BREAK
- endif
-
- /* if target pointer is 0, we must do a copy. When != 0, we have to
- compare the DATE and TIME stamps on both files to determine if the
- copy should be processed.
- */
- if nTgtPtr == 0
- lDoCopy := .T.
- else
- // load subarrays
- aSrc := aDirSrc[nSrcPtr]
- aTgt := aDirTgt[nTgtPtr]
-
- // check dates; if the same, compare times
- if aSrc[F_DATE] == aTgt[F_DATE]
- lDoCopy := lTimeStp( aTgt[F_TIME], aSrc[F_TIME])
- elseif aSrc[F_DATE] > aTgt[F_DATE]
- lDoCopy := .T.
- endif
- endif
-
- /* if interactive flag is set, we must ask the user for
- permission UNLESS in Over-Ride mode
- */
- if dict->INTERACT .and. lDoCopy .and. ! lOverride
- if alert("Do you wish to synchronize file:;" + ;
- cFileName, {"YES","no "}) == 1
- lDoCopy := .T.
- else
- lDoCopy := .F.
- endif
- endif
-
- END SEQUENCE
-
- /* Do the copy if required. This is actually done by a batch file
- which will be called after this program terminates.
- */
- if lDoCopy
- //1st normalize source path
- if right(alltrim(dict->SOURCE_DIR), 1) == "\"
- cSrcFile := alltrim( dict->SOURCE_DIR ) + ;
- alltrim( cFileName )
- else
- cSrcFile := alltrim( dict->SOURCE_DIR ) + ;
- "\" + alltrim( cFileName )
- endif
-
- //2nd normalize target path
- if right(alltrim(dict->TARGET_DIR), 1) == "\"
- cTgtFile := alltrim( dict->TARGET_DIR ) + ;
- alltrim( cFileName )
- else
- cTgtFile := alltrim( dict->TARGET_DIR ) + ;
- "\" + alltrim( cFileName )
- endif
-
- /* 3rd normalize the daisy-chain path... this path, if non-
- blank will be the target for the previous target file.
- */
- if !empty( dict->SHADOW_DIR ) .and. ;
- right(alltrim(dict->SHADOW_DIR), 1) == "\"
- cSHADOWFl := alltrim( dict->SHADOW_DIR ) + ;
- alltrim( cFileName )
- elseif !empty( dict->SHADOW_DIR )
- cSHADOWFl := alltrim( dict->SHADOW_DIR ) + ;
- "\" + alltrim( cFileName )
- else
- cSHADOWFl := ""
- endif
-
- /* Check to see if any files are in the target directory. To
- do this, we'll use Clippers file() function with the file
- specification "NUL" which exists in all DOS directories.
- If not, build the directory(ies) and place name in array
- if not unique. This check avoids multiple MKDIRs to the
- same directory. A typical directory drive:\path\path\path
- will require three MKDIRs to be issued to DOS to insure
- that the structure is built correctly:
- mkdir drive:\path
- mkdir drive:\path\path
- mkdir drive:\path\path\path
-
- We'll handle this mess by creating a null array and passing
- the path to depth() which will deal with the substring
- extractions. An element will be created for each required path.
-
- *** REMOVE THE CHECK FOR FILE() IF THE ***
- *** SHADOW DIRECTORY IS VIRTURAL AND ***
- *** YOU CANNOT ENSURE THAT THE DRIVE WILL ***
- *** BE MOUNTED/AVAILABLE AT SYNC RUN ***
- */
- if ! file( substr( cWildTgt, 1, len( cWildTgt ) - 3) + "NUL." )
- if ascan( aMakDir, upper( alltrim( dict->TARGET_DIR ))) == 0
- aMakeArray := {} // initialize
- aMakeArray := depth( cWildTgt )
-
- for nLooper := 1 to len( aMakeArray )
- // only create necessary level(s)
- if ascan( aMakDir, aMakeArray[ nLooper ] ) == 0
- // need to create a directory
- aadd( aWriteOut, "MKDIR " + ;
- aMakeArray[ nLooper ])
- aadd( aMakDir, aMakeArray[ nLooper ] )
- endif
- next
- endif
- endif
-
- /*
- *** REMOVE THE CHECK FOR FILE() IF THE ***
- *** SHADOW DIRECTORY IS VIRTURAL AND ***
- *** YOU CANNOT ENSURE THAT THE DRIVE WILL ***
- *** BE MOUNTED/AVAILABLE AT SYNC RUN ***
- */
- if !empty( cSHADOWFl ) .and. ;
- !file( substr( cWildSHADOW, 1, len( cWildSHADOW ) - 4) + "NUL." )
- if ascan( aMakDir, upper( alltrim( dict->SHADOW_DIR ))) == 0
- aMakeArray := {} // zap the array
- aMakeArray := depth( cWildSHADOW )
- for nLooper := 1 to len( aMakeArray )
- if ascan( aMakDir, aMakeArray[ nLooper ] ) == 0
- aadd( aWriteOut, "MKDIR " + ;
- aMakeArray[ nLooper ])
- aadd( aMakDir, aMakeArray[ nLooper ] )
- endif
- next
- endif
- endif
-
- /* create an entry in the output array for a single DOS copy
- and increment the file update counter
- */
- aadd( aWriteOut, "COPY " + CSRCFILE + " " + CTGTFILE )
- ++nCount
-
- // handle the copy to a shadow directory copy if necessary
- if !empty( cSHADOWFl )
- aadd( aWriteOut, "COPY " + CTGTFILE + " " + CSHADOWFL )
- ++nCount
- endif
- endif
-
- next i // continue loop
-
- // now update the dictionary to show current date and time
- replace LASTDATE with date(), LASTTIME with time()
-
- if lTime
- aadd( eTime, "SkipRec:" + time() + " Item#" + ;
- str( nCount, 3, 0 ))
- endif
-
- skip // to next dictionary database specification record
- Enddo
-
- WrapItUp( cOutBatch, nCount )
- dbCloseALL() // close database, purge buffers
- erase c:\temp.$$$ // house clean...
-
- //report status if not in quiet mode
- if ! lQuiet .and. nCount > 0
- ? str(nCount,3,0),"files require updating!"
- ? "Process batch file: ", cOutBatch
- endif
-
- // End-Of-Program SYNC.EXE No return value to DOS
- return NIL
-
- function lTimeStp( cTime1, cTime2 )
- // character format as hh:mm:ss
- // calc as cT2 - cT1
- // return True if 2 > 1
- local T1h := val(substr(cTime1,1,2)) * 3600
- local T1m := val(substr(cTime1,4,2)) * 60
- local T1s := val(substr(cTime1,7,2)) + T1m + T1h
- local T2h := val(substr(cTime2,1,2)) * 3600
- local T2m := val(substr(cTime2,4,2)) * 60
- local T2s := val(substr(cTime2,7,2)) + T2m + T2h
- local lValue := T2s > T1s
- return (lValue)
-
- function mEdit()
- local cOldColour, adBase := {}
- local lRetVal := .F.
-
- if file("C:\SYNC.MEM")
- restore from c:\sync additive // D_LOOK
- D_LOOK := left(D_LOOK + space(60), 60)
- else
- D_LOOK := space(60)
- endif
-
- clear screen
- @ 10, 5 say "File C:\SYNC.MEM must contain the fully specified path to"
- @ 11, 5 say "the location of the synchronization database: SYNC.DBF"
- @ 12, 5 say "Example: Z:\PUBLIC\MISC\SYNC.DBF"
- @ 15, 5 say "Location:" get D_LOOK picture "@!"
- read
-
- save all like D_LOOK to C:\SYNC.MEM
-
- @ 17, 5 say "---> Memory Pointer Created/Updated"
-
- // now ask if the database needs creating at proper location
- if ! file( D_LOOK) .and. ;
- alert("Create the database structure now?", ;
- {"YES","no "} ) == 1
- aadd( adBase, { "FILENAME", "C", 12, 0 })
- aadd( adBase, { "SOURCE_DIR", "C", 40, 0 })
- aadd( adBase, { "TARGET_DIR", "C", 40, 0 })
- aadd( adBase, { "SHADOW_DIR", "C", 40, 0 })
- aadd( adBase, { "INTERACT", "L", 1, 0 })
- aadd( adBase, { "LASTDATE", "D", 8, 0 })
- aadd( adBase, { "LASTTIME", "C", 8, 0 })
- dbcreate( d_LOOK, adBase )
- @ 18, 5 say "---> Database Structure Created"
- if alert("Edit Empty Database Now?", ;
- { "YES", "no " }) == 1
- use ( D_LOOK ) new
- dEdit()
- dbCloseArea()
- lRetVal := .T.
- endif
- else
- lRetVal := .T.
- endif
- return ( lRetVal )
-
- function depth( cPathString )
- /* DOS is brain dead in some respects. This routine will create an
- array which will have one element for each subordinate directory
- nesting to the lowest level. For example, if the string passed was
- c:\windows\system\stuff\*.*
- then the array would be returned as:
- [1] c:\windows
- [2] c:\windows\system
- [3] c:\windows\system\stuff
- thus allowing us to create the new structure prior to attempting the
- desired copies. We'll return a pointer to the array and reassign the
- name in the main routine. If the string passed was c:\*.*, then the
- routine will return a null array indicating that no directories are
- to be built.
- */
-
- local aArray := {}
- local cTemp := "", nLoop, cDrvPrefix := ""
-
- // handle the special cases here
- if substr( cPathString, 2 ) == ":*.*" .or. ;
- substr( cPathString, 2 ) == ":\*.*"
- RETURN aArray
- endif
-
- /* remove the drive prefix and correct for syntax. For example, both
- c:\windows and c:windows are acceptable
- */
- cDrvPrefix := upper( substr( cPathString, 1, 2 ))
- if substr( cPathString, 3, 1 ) == "\"
- cPathString := upper( substr( cPathString, 3))
- else
- cPathString := "\" + upper( substr( cPathString, 3))
- endif
-
- /* Remove the wildcard on end of string; that is \*.*
- When complete, we'll have a path that looks like
- \WINDOWS\SYSTEM\STUFF
- */
- cPathString := substr( cPathString, 1, len( cPathString ) - 4 )
-
- /* Now add the drive prefix back to create a fully specified path with
- a known (normalized) structure. For example
- C:\WINDOWS\SYSTEM\STUFF
- */
- cPathString := cDrvPrefix + cPathString
-
- /* determine the nesting levels and initialize each array element of
- receptor array. When finished the stack will be:
- C:\WINDOWS : handled by looping
- C:\WINDOWS\SYSTEM : handled by looping
- C:\WINDOWS\SYSTEM\STUFF : brute force addition after loop
- */
- for nLoop = 4 to len( cPathString )
- if substr( cPathString, nLoop, 1 ) == "\"
- /*
- Only build structures that are required. Look
- to see if files exist in the possible directory
- combinations.
-
- REMOVE THE TEST FOR FILE() IF TARGET DRIVE IS
- VIRTURAL AND YOU CANNOT ENSURE THAT THE DRIVE
- WILL BE AVAILABLE AT RUN TIME
- */
- if !file( substr( cPathString , 1, nLoop -1 ) + "\NUL." )
- aadd( aArray, ;
- substr( cPathString , 1, nLoop -1 ))
- endif
- endif
- next
-
- aadd( aArray, cPathString ) // last level to create!
-
- RETURN ( aArray ) // return a reference
-
- function Raw_Prn( cOut )
- // handle printer in the raw!
- fwrite(4, cOut, len(cOut))
- return NIL
-
- function report( lVideo )
- local nLineCt := 0
-
- go top
- do while ! eof()
- if lVideo
- if nLineCt == 0 ; clear screen ; endif
- ? "File:",dict->FILENAME, ;
- "Date:",dict->LASTDATE, ;
- "Time:",dict->LASTTIME
- ++nLineCt
- if nLineCt % 20 == 0
- @ 24, 0 say "Press any key...."
- inkey(0)
- nLineCt := 0
- endif
- else
- Raw_Prn( "File: " + dict->FILENAME + ;
- "Date: " + dtoc( dict->LASTDATE ) + ;
- "Time: " + dict->LASTTIME + ;
- CRLF )
- ++nLineCt
- if nLineCt % 55 == 0 ; Raw_Prn ( FF ) ; endif
- endif
-
- skip
- loop
- enddo
- Return NIL
-
- function WrapItUP( cOutBatch, nCount )
- local nLoop
-
- if nCount == 0
- RETURN NIL
- else
- /* Open ASCII file for text output and later batch processing. I
- could have chosen to use low-level file functions but was a bit
- lazy at this point.
- */
-
- aadd( aWriteOut, "REM end-of-file. " + CRLF )
-
- SET CONSOLE OFF // disable screen echo
- set alternate to ( cOutBatch )
- set alternate ON // echo output to text file
-
- ?? aWriteOut[ 1 ]
-
- for nLoop = 2 to len( aWriteOut )
- ? aWriteOut[ nLoop ]
- next
-
- /* If operator used /T to analyze run time, then loop through
- that array, too
- */
- if lTime
- for nLoop = 1 to len( eTime )
- ? "REM ", eTime[ nLoop ]
- next
- ? "Finish: " + time()
- endif
-
- ? ""
- set alternate OFF // closes ASCII output
- set alternate TO // release file handle
- set console ON // return video display
- endif
-
- RETURN NIL
-
- function dEdit()
- browse() // edit in table format [ it's OK! Now Tbrowse in 5.2b ]
- pack // remove deleted records
- dbcommit() // purge DOS buffers to disk
- Return NIL
-